#include "Simulation.h"

#include "./ServerEventsDispatcher.h"

#include "../Graphics/Presentation.h"
#include "../Input/InputThread.h"
#include "../Common/AngleOps.h"
#include "./Objects/Factory.h"
#include "../Core/PlayerArray.h"

#include "../Commands/Update.h"

#include "../Common/Vec2dOgreExt.h"

#include "../Network/ConnectionPool.h"

#define BROADCASTING_INTERVAL 500

using namespace GameLogic;

Simulation::Simulation( bool is_server ):
	command_queue_( Commands::Queue::getInstance() ),
	is_server_( is_server ),
	last_broadcast_( 0 ),
	level_( 0 ),
	ray_scene_query_( Graphics::Presentation::getInstance().getSceneManager()->createRayQuery( Ogre::Ray() ) ),
	last_mouse_position_( 0, 0 ),
	now_( 0 )
{}

Simulation::~Simulation() {
	Graphics::Presentation::getInstance().getSceneManager()->destroyQuery( ray_scene_query_ );
}

void Simulation::broadcastData() {
	// @networking - okresowe rozsylanie danych przez serwer.
	Level::ObjectMapIter end = level_->objectsEnd();
	for( Level::ObjectMapIter obj = level_->objectsBegin(); obj != end; ++obj ) {
		Objects::GameObject* go = obj->second;
		if( go->isModified() ) {
			go->setModified( false );
			Commands::Command *cmd = new Commands::Update( obj->first, obj->second->getData() );
			Network::ConnectionPool::getInstance().broadcast( cmd );
		}
	}
}

void Simulation::receiveCommands() {
	Commands::Command* c;
	while( ( c = command_queue_.pop() ) != 0 ) {
		c->execute( this );
		delete c;
	}
}

void Simulation::simulate( long dt ) {
	now_ += dt;

	//buforuje sobie aktora sterowanego przez gracza:
	Objects::GameObject* player_object = Core::PlayerArray::getInstance().getLocalPlayer()->controlled_object;
	//zapamietujemy znaczniki zachowania gracza:
	int player_behaviour = player_object->getBehaviourFlags();

	//I - odbiera komendy:
	receiveCommands();

	//II - reaguje na wejscie: obraca postac gracza w zaleznosci od ustawienie kursora
	bool rot_changed = false;
	Vec2i mouse_position = Input::InputThread::getInstance().getMousePosition();
	if( mouse_position != last_mouse_position_ ) {
		mouse_position = last_mouse_position_;

		/* Tworzymy RaySceneQuery: */
		Graphics::Presentation& presentation = Graphics::Presentation::getInstance();
		float window_width = static_cast<float>( presentation.getWindow()->getWidth() );
		float window_height = static_cast<float>( presentation.getWindow()->getHeight() );
		Ogre::Ray mouse_ray = presentation.getCamera()->getCameraToViewportRay( mouse_position.x / window_width, mouse_position.y / window_height );
		ray_scene_query_->setRay( mouse_ray );

		/* Odpalamy zapytanie: */
		Ogre::RaySceneQueryResult &result = ray_scene_query_->execute();
		Ogre::RaySceneQueryResult::iterator iter = result.begin();

		/* ...i ostatecznie analizujemy wynik: */
		if( iter != result.end() && iter->worldFragment ) {
			Ogre::Vector3& vp = iter->worldFragment->singleIntersection;
			const Vec2f& vo = player_object->getPosition();
			Vec2f v = vp - vo;
			player_object->setFacing( v.normalize() );
			rot_changed = true;
		}
	}

	/* Przestarzale - o ile obracamy swobodnie postaci gracza, ten kod moze byc zastapiony przez InputDispatchera

	//II i pol - jezeli zaszla zmiana w propagowalnych zmianach obiektu i jestesmy
	//           klientem - informujemy serwer.
	if( !is_server_ ) {
		bool bhv_changed = false;
		if( player_object->getBehaviourFlags() != player_behaviour )
			bhv_changed = true;
		//w zaleznosci od tego, co zostalo zmienione:
		if( bhv_changed && rot_changed && player_object->isBehaviourChangedByPlayerInput() ) {
			player_object->setBehaviourChangedByPlayerInput( false );
			//TODO: wysylamy wiadomosc: zmieniony obrot gracza oraz znaczniki zachowania

		} else {
			if( bhv_changed && player_object->isBehaviourChangedByPlayerInput() ) {
				player_object->setBehaviourChangedByPlayerInput( false );
				//TODO: wysylamy wiadomosc: zmienione flagi zachowania

			}
			if( rot_changed ) {
				//TODO: wysylamy wiadomosc: zmieniony obrot

			}
		}
	}

	 */

	//III - symuluje obiekty
	Level::ObjectMapIter end_obj = level_->objectsEnd();
	for( Level::ObjectMapIter obj = level_->objectsBegin(); obj != end_obj; ++obj )
		simulateObject( obj->second, dt );

	//IV - sprawdza kolizje
	for( Level::ObjectMapIter obj = level_->objectsBegin(); obj != end_obj; ++obj ) {
		Objects::GameObject* go = obj->second;
		if( go->getCollisionState() == Objects::GameObject::csNotChecked ) {
			checkCollisions( go );
		}
	}

	//V - propagacja stanu serwera
	if( is_server_ ) {
		last_broadcast_ += dt;
		if( last_broadcast_ > BROADCASTING_INTERVAL ) {
			broadcastData();
			last_broadcast_ -= BROADCASTING_INTERVAL;
		}
	}

} //simulate()

void Simulation::updateObject( Objects::GameObject::IdType id, Data* data ) {
	Objects::GameObject* go = level_->getObject( id );
	if( go != 0 ) {
		//obiekt istnieje:
		//...
		long time_diff = data->time - go->getTime();
		//...
		if( time_diff > 0 ) {
			//oblicza czas potrzebny, aby przesunac obiekt z czasu waznosci nowych danych do teraz:
			time_diff = now() - data->time;
			//cofa obiekt do stanu na chwile ostatniej aktualizacji:
			simulateObject( go, -time_diff );
			//aplikuje nowe Data:
			go->replaceData( data );
			//symuluje obiekt do aktualnego czasu:
			simulateObject( go, time_diff );

		} else if( time_diff == 0 ) {
			//czasy idealnie sie zgadzaja. Coz za szczesliwa niespodzianka:
			//nie trzeba nic aktualizowac, wystarczy podmienic dane.
			go->replaceData( data );
		} else {
			//Odebralismy zdarzenie z przyszlosci. Ignorujemy.
		}

	} else {
		//obiekt nie istnieje:
		//...wiec go tworzy:
		go = Objects::Factory::getInstance().createObject( id, data );
		//...dodaje do listy obiektow na planszy:
		level_->insertObject( go );
		//...no i wreszcie przypina do stosownych tili:
		Vec2i tile;
		Vec2f offset;
		getObjectPositionAndTileOffset( go, tile, offset );
		installTileHooks( go, tile, offset );
		//...koniec koncow symulujemy obiekt tak, aby znalazl sie w obecnej chwili:
		long time_diff = now() - data->time;
		if( time_diff != 0 )
			simulateObject( go, time_diff );
	}

	go->positionOnScene();
}

void Simulation::getObjectPositionAndTileOffset( Objects::GameObject* go, Vec2i& tile, Vec2f& offset ) {
	getObjectPosition( go, tile );
	const Vec2f& pos = go->getPosition();
	float tile_size = level_->getTileSize();
	//Przesuniecie w tile'u:
	offset.x = fmod( pos.x, tile_size );
	offset.y = fmod( pos.y, tile_size );
}

void Simulation::checkCollisions( Objects::GameObject* go ) {
	go->setCollisionState( Objects::GameObject::csChecked );
	//sprawdza kolizje z terenem:
	Vec2i tile;
	Vec2f offset;
	getObjectPositionAndTileOffset( go, tile, offset );
	if( isValidTileCoord( tile ) ) {
		float radius = go->getRadius();
		//sprawdzenie na osi x:
		if( go->getBehaviourFlags() & (Objects::GameObject::FLAG_LEFT | Objects::GameObject::FLAG_RIGHT) ) {
			if( offset.x - radius < 0.0f ) {
				if( !tileIsEnterable( tile.x - 1, tile.y, Level::Tile::tsRight ) ) {
					go->translate( Vec2f( - (offset.x - radius), 0.0f ) );
				}
			} else if( offset.x + radius > level_->getTileSize() ) {
				if( !tileIsEnterable( tile.x + 1, tile.y, Level::Tile::tsLeft ) ) {
					go->translate( Vec2f( level_->getTileSize() - (offset.x + radius), 0.0f ) );
				}
			}
		}
		//sprawdzenie na osi y:
		if( go->getBehaviourFlags() & (Objects::GameObject::FLAG_UP | Objects::GameObject::FLAG_DOWN) ) {
			if( offset.y - radius < 0.0f ) {
				if( !tileIsEnterable( tile.x, tile.y - 1, Level::Tile::tsBottom ) ) {
					go->translate( Vec2f( 0.0f, - (offset.y - radius) ) );
					if( is_server_ )
						ServerEventsDispatcher::dispatchOnHitImpassableTile( go );
				}
			} else if( offset.y + radius > level_->getTileSize() ) {
				if( !tileIsEnterable( tile.x, tile.y + 1, Level::Tile::tsTop ) ) {
					go->translate( Vec2f( 0.0f, level_->getTileSize() - (offset.y + radius) ) );
				}
			}
		}
	}

	//sprawdza kolizje z innymi obiektami:
	Level::Tile** hooks = go->getTileHooks();
	for( int i = 0; i < 4; ++i ) {
		if( hooks[i] != 0 ) {
			Level::Tile::MapIter end = hooks[i]->lastObject();
			for( Level::Tile::MapIter obj = hooks[i]->firstObject() ; obj != end; ++obj ) {
				Objects::GameObject* go2 = obj->second;
				if( go2->getCollisionState() == Objects::GameObject::csNotChecked ) {
					//obiekt nie byl symulowany. Testuje kolizje z tym obiektem:
					Vec2f intersection_vector;
					if( intersects( go, go2, intersection_vector ) ) {
						go->translate( intersection_vector );
						//jezeli jestesmy na serwerze, nalezy odpalic zdazenia:
						if( is_server_ ) {
							ServerEventsDispatcher::dispatchOnHit( go, go2 );
						}
					}
				}
			}
		}
	}
} //checkCollisions()

/*
 * Analizuje sterowanie obiektu i stosownie przesuwa go.
 */
void Simulation::simulateObject( Objects::GameObject* go, long dt ) {
	go->resetTemporaryData();

	int bhv = go->getBehaviourFlags();
	Vec2f displacement_vector;

	if( bhv & Objects::GameObject::FLAG_UP ) {
		Vec2f strafe_vector = Vec2f::instantiateRotatedVector( go->getStrafeSpeed() * dt, toRad( 90.0f ) );
		displacement_vector += strafe_vector;
	} else

	if( bhv & Objects::GameObject::FLAG_DOWN ) {
		Vec2f strafe_vector = Vec2f::instantiateRotatedVector( go->getStrafeSpeed() * dt, toRad( -90.0f ) );
		displacement_vector += strafe_vector;
	} else

	if( bhv & Objects::GameObject::FLAG_LEFT ) {
		Vec2f strafe_vector = Vec2f::instantiateRotatedVector( go->getStrafeSpeed() * dt, toRad( 0.0f ) );
		displacement_vector += strafe_vector;
	} else

	if( bhv & Objects::GameObject::FLAG_RIGHT ) {
		Vec2f strafe_vector = Vec2f::instantiateRotatedVector( -go->getStrafeSpeed() * dt, toRad( 0.0f ) );
		displacement_vector += strafe_vector;
	}

	//trzeba cos robic tylko, jezeli przesuniecie bylo niezerowe
	if( (displacement_vector.x != 0.0f) || (displacement_vector.y != 0.0f) ) {

		//sprawdza polozenie obiektu przed przesunieciem:
		Vec2i tile1;
		getObjectPosition( go, tile1 );

		//przesuwa obiekt:
		go->translate( displacement_vector );

		//sprawdza polozenie obiektu po przesunieciu:
		Vec2i tile2;
		Vec2f offset2;
		getObjectPositionAndTileOffset( go, tile2, offset2 );

		//jezeli nastapila zmiana tile'a, przepina obiekt:
		if( tile1 != tile2 ) {
			//odpina obiekt:
			releaseTileHooks( go );
			//przypina obiekt:
			installTileHooks( go, tile2, offset2 );
		}

		go->setModified( true );
	} else {
		//w przeciwnym wypadku mozna obiekt oznaczyc jako statyczny i np. pominac sprawdzanie
		//kolizji dla niego.
	}
} // simulateObject()

void Simulation::attachObjectToTile( Objects::GameObject* go, Level::Tile** tile_hook, int x, int y ) {
	if( (x >= 0) && (y >= 0) && (x < level_->getWidth()) && (y < level_->getHeight()) ) {
		Level::Tile* t = level_->getTile( x, y );
		*tile_hook = t;
		(*tile_hook)->pin( go );
	}
}

void Simulation::installTileHooks( Objects::GameObject* go, const Vec2i& tile, const Vec2f& offset ) {
	/*
	 * hooks w formacie:
	 * [ 0 , 1 ]
	 * [ 2 , 3 ]
	 */
	Level::Tile** hooks = go->getTileHooks();
	//pol wielkosci tile'a:
	float half_tile = 0.5f * level_->getTileSize();
	//wybiera tile, do ktorych nalezy doczepic obiekt:
	if( offset.x < half_tile ) {
		//obiekt po prawej stronie siatki
		if( offset.y < half_tile ) {
			//obiekt na dole
			attachObjectToTile( go, &( hooks[ 3 ] ), tile.x, tile.y );
			attachObjectToTile( go, &( hooks[ 2 ] ), tile.x-1, tile.y );
			attachObjectToTile( go, &( hooks[ 1 ] ), tile.x, tile.y-1 );
			attachObjectToTile( go, &( hooks[ 0 ] ), tile.x-1, tile.y-1 );
		} else {
			//obiekt na gorze
			attachObjectToTile( go, &( hooks[ 1 ]), tile.x, tile.y );
			attachObjectToTile( go, &( hooks[ 2 ]), tile.x-1, tile.y+1 );
			attachObjectToTile( go, &( hooks[ 3 ]), tile.x, tile.y+1 );
			attachObjectToTile( go, &( hooks[ 0 ]), tile.x-1, tile.y );
		}
	} else {
		//obiekt po lewej stronie siatki
		if( offset.y < half_tile ) {
			//obiekt na dole
			attachObjectToTile( go, &( hooks[ 2 ]), tile.x, tile.y );
			attachObjectToTile( go, &( hooks[ 3 ]), tile.x+1, tile.y );
			attachObjectToTile( go, &( hooks[ 0 ]), tile.x, tile.y-1 );
			attachObjectToTile( go, &( hooks[ 1 ]), tile.x+1, tile.y-1 );
		} else {
			//obiekt na gorze
			attachObjectToTile( go, &( hooks[ 0 ]), tile.x, tile.y );
			attachObjectToTile( go, &( hooks[ 1 ]), tile.x+1, tile.y );
			attachObjectToTile( go, &( hooks[ 2 ]), tile.x, tile.y+1 );
			attachObjectToTile( go, &( hooks[ 3 ]), tile.x+1, tile.y+1 );
		}
	}
} // installTileHooks()
